在 pubspec.yaml 中加入 Flutter 官方維護的 WebSocket 處理套件:
dependencies:
web_socket_channel: ^3.0.1
執行 flutter pub get
websocket_service.dart)建立 lib/services/websocket_service.dart,負責處理 WebSocket 的連線、傳送音訊與監聽文字:
import 'dart:async';
import 'dart:typed_data';
import 'package:web_socket_channel/web_socket_channel.dart';
class WebSocketService {
WebSocketChannel? _channel;
StreamSubscription? _subscription;
/// 1. 連線至 STT WebSocket 伺服器
Future<void> connect(String url, Function(String textChunk) onTextReceived) async {
try {
_channel = WebSocketChannel.connect(Uri.parse(url));
print('WebSocket 連線成功:$url');
// 監聽伺服器即時傳回來的逐字稿(文字流)
_subscription = _channel!.stream.listen(
(message) {
print('收到即時逐字稿: $message');
onTextReceived(message.toString());
},
onError: (error) => print('WebSocket 錯誤: $error'),
onDone: () => print('WebSocket 連線已關閉'),
);
} catch (e) {
print('連線失敗: $error');
}
}
/// 2. 將 Day 9 的 Audio Chunk (位元組) 即時送出
void sendAudioChunk(Uint8List chunk) {
if (_channel != null) {
_channel!.sink.add(chunk); // 直接把 Byte 串流塞進管道
}
}
/// 3. 斷開連線與釋放資源
Future<void> disconnect() async {
await _subscription?.cancel();
await _channel?.sink.close();
_channel = null;
print('已中斷 WebSocket 連線');
}
}
recorder_page.dart)將 Day 9 的 AudioRecorderService 與今天的 WebSocketService 結合,實現邊錄邊傳:
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'services/audio_recorder_service.dart';
import 'services/websocket_service.dart';
class AudioStreamPage extends StatefulWidget {
const AudioStreamPage({super.key});
@override
State<AudioStreamPage> createState() => _AudioStreamPageState();
}
class _AudioStreamPageState extends State<AudioStreamPage> {
final AudioRecorderService _recorderService = AudioRecorderService();
final WebSocketService _webSocketService = WebSocketService();
bool _isStreaming = false;
String _liveTranscript = ''; // 即時顯示的逐字稿內容
// 測試用的免費 WebSocket 模擬伺服器(或換成你自己的後端)
final String _wsUrl = 'wss://echo.websocket.org';
void _toggleStreaming() async {
if (_isStreaming) {
// 停止錄音與傳輸
await _recorderService.stopRecording();
await _webSocketService.disconnect();
setState(() => _isStreaming = false);
} else {
setState(() {
_isStreaming = true;
_liveTranscript = ''; // 清空上次結果
});
// 1. 建立 WebSocket連線
await _webSocketService.connect(_wsUrl, (String textChunk) {
setState(() {
// 收到邊辨識邊吐出的字,持續累加到畫面上
_liveTranscript += textChunk;
});
});
// 2. 開始錄音,並把每個 Audio Chunk 即時丟給 WebSocket
await _recorderService.startChunkedRecording((Uint8List chunk) {
_webSocketService.sendAudioChunk(chunk);
});
}
}
@override
void dispose() {
_recorderService.dispose();
_webSocketService.disconnect();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Day 11 - 即時語音串流逐字稿')),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
const SizedBox(height: 30),
Icon(
_isStreaming ? Icons.record_voice_over : Icons.mic_none,
size: 80,
color: _isStreaming ? Colors.green : Colors.grey,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _toggleStreaming,
style: ElevatedButton.styleFrom(
backgroundColor: _isStreaming ? Colors.red : Colors.green,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
),
child: Text(
_isStreaming ? '停止串流' : '開始即時串流對話',
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
const SizedBox(height: 40),
const Align(
alignment: Alignment.centerLeft,
child: Text('即時逐字稿顯示:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
),
const SizedBox(height: 10),
Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black12,
borderRadius: BorderRadius.circular(12),
),
child: SingleChildScrollView(
child: Text(
_liveTranscript.isEmpty ? '按下按鈕並開始說話...' : _liveTranscript,
style: const TextStyle(fontSize: 18, height: 1.4),
),
),
),
),
],
),
),
);
}
}
🎙️ 收到 Audio Chunk ... ➔ 📩 收到即時逐字稿 ...